import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_pagewise/flutter_pagewise.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:my_flutter_core/utils/connectivity_utils.dart';
import 'package:my_flutter_core/widgets/my_progress_indicator.dart';

/// Exposes the [PagewiseLoadController] to client for any necessary interaction.
typedef GetLoadController = void Function(dynamic loadController);

/// Used for retrieving data when list view scrolls down to the bottom.
typedef RetrieveData = Future<List<dynamic>> Function(int? pageIndex);

/// Builds the item for the list view. Normally it returns a [ListTile].
typedef PagingViewItemBuilder =
    Widget Function(BuildContext context, dynamic entry, int index);

/// Builds the item for the animated list view. Normally it returns a [ListTile].
typedef PagingAnimatedListItemBuilder =
    Widget Function(
      BuildContext context,
      int index,
      dynamic item,
      Animation<double> animation,
    );

/// Wrapper for [PagewiseLoadController]. Client can interact with it if needed.
class PagingListViewLoadController {
  PagingListViewLoadController(this.loadController);

  final PagewiseLoadController loadController;

  void reset() {
    loadController.reset();
  }
}

class NoItemFound extends StatelessWidget {
  const NoItemFound({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Icon(
          MdiIcons.informationVariantCircleOutline,
          size: 40.0,
          color: Colors.grey[400],
        ),
        const SizedBox(height: 6.0),
        const Text(
          '暂无数据',
          style: TextStyle(color: Colors.grey),
          textAlign: TextAlign.center,
        ),
      ],
    );
  }
}

/// A proxy list view to external list view widget.
/// We can customize this list view without updating the biz code.
///
/// [pageSize] determines how many items needed from server within one round trip.
/// [retrieveData] is the function which used to retrieve the data. It returns [Future].
/// [itemBuilder] is the function which builds the list item for display.
/// [getLoadController] is a callback function to supply the [PagewiseLoadController] for any advance usage.
class PagingListView extends StatefulWidget {
  const PagingListView({
    super.key,
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    this.noItemsFoundBuilder,
    this.padding = const EdgeInsets.all(0.0),
    this.getLoadController,
    this.disconnected,
    this.refreshed,
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingViewItemBuilder itemBuilder;
  final NoItemsFoundBuilder? noItemsFoundBuilder;
  final EdgeInsetsGeometry padding;
  final GetLoadController? getLoadController;
  final VoidCallback? disconnected;
  final VoidCallback? refreshed;

  @override
  State<PagingListView> createState() => _PagingListViewState();
}

class _PagingListViewState extends State<PagingListView> {
  // Using [PagewiseLoadController] due to the needs of [RefreshIndicator].
  // We need to support the refresh operation when user pulls down on the top of the list.
  late final PagewiseLoadController _loadController = PagewiseLoadController(
    pageSize: widget.pageSize,
    pageFuture: widget.retrieveData,
  );

  @override
  void initState() {
    super.initState();

    widget.getLoadController?.call(
      PagingListViewLoadController(_loadController),
    );
  }

  @override
  void dispose() {
    _loadController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      child: PagewiseListView(
        padding: widget.padding,
        itemBuilder: widget.itemBuilder,
        noItemsFoundBuilder:
            widget.noItemsFoundBuilder ?? (context) => const NoItemFound(),
        pageLoadController: _loadController,
      ),
      onRefresh: () async {
        // Checks on network connection when refreshing on off-line mode.
        // If network connection available then rebuild UI to switch to on-line paging list view.
        // If not a toast will be shown.
        if (await hasNetworkConnection() == false) {
          widget.disconnected?.call();
        } else {
          _loadController.reset();
          widget.refreshed?.call();
        }
      },
    );
  }
}

/// A proxy list view to external list view widget.
/// We can customize this list view without updating the biz code.
///
/// [pageSize] determines how many items needed from server within one round trip.
/// [retrieveData] is the function which used to retieve the data. It returns [Future].
/// [itemBuilder] is the function which builds the list item for display.
/// [getLoadController] is a callback function to supply the [PagewiseLoadController] for any advance usage.
class PagingSliverListView extends StatefulWidget {
  const PagingSliverListView({
    super.key,
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    this.noItemsFoundBuilder,
    this.getLoadController,
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingViewItemBuilder itemBuilder;
  final NoItemsFoundBuilder? noItemsFoundBuilder;
  final GetLoadController? getLoadController;

  @override
  State<PagingSliverListView> createState() => _PagingSliverListViewState();
}

class _PagingSliverListViewState extends State<PagingSliverListView> {
  // Using [PagewiseLoadController] due to the needs of [RefreshIndicator].
  // We need to support the refresh operation when user pulls down on the top of the list.
  late final PagewiseLoadController _loadController = PagewiseLoadController(
    pageSize: widget.pageSize,
    pageFuture: widget.retrieveData,
  );

  @override
  void initState() {
    super.initState();

    widget.getLoadController?.call(
      PagingListViewLoadController(_loadController),
    );
  }

  @override
  void dispose() {
    _loadController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return PagewiseSliverList(
      itemBuilder: widget.itemBuilder,
      noItemsFoundBuilder:
          widget.noItemsFoundBuilder ?? (context) => const NoItemFound(),
      pageLoadController: _loadController,
    );

    // With Flutter 1.7 or higher version the [RefreshIndicator] cannot be used with [CustomScrollView].
    // Consider to use [NestedScrollView] instead.
    // For full example check on [IncomeExpenseSearchResult].
    // return RefreshIndicator(
    //   child: PagewiseSliverList(
    //     itemBuilder: widget.itemBuilder,
    //     pageLoadController: _loadController,
    //   ),
    //   onRefresh: () async {
    //     _loadController.reset();
    //   },
    // );
  }
}

/// A proxy list view to external list view widget.
/// We can customize this list view without updating the biz code.
///
/// [pageSize] determines how many items needed from server within one round trip.
/// [retrieveData] is the function which used to retieve the data. It returns [Future].
/// [itemBuilder] is the function which builds the list item for display.
/// [getLoadController] is a callback function to supply the [PagewiseLoadController] for any advance usage.
class PagingGridView extends StatefulWidget {
  const PagingGridView({
    super.key,
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    this.noItemsFoundBuilder,
    this.crossAxisCount = 2,
    this.childAspectRatio = 1.0,
    this.padding = const EdgeInsets.all(0.0),
    this.getLoadController,
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingViewItemBuilder itemBuilder;
  final NoItemsFoundBuilder? noItemsFoundBuilder;
  final int crossAxisCount;
  final double childAspectRatio;
  final EdgeInsetsGeometry padding;
  final GetLoadController? getLoadController;

  @override
  State<PagingGridView> createState() => _PagingGridViewState();
}

class _PagingGridViewState extends State<PagingGridView> {
  // Using [PagewiseLoadController] due to the needs of [RefreshIndicator].
  // We need to support the refresh operation when user pulls down on the top of the list.
  late final PagewiseLoadController _loadController = PagewiseLoadController(
    pageSize: widget.pageSize,
    pageFuture: widget.retrieveData,
  );

  @override
  void initState() {
    super.initState();

    widget.getLoadController?.call(
      PagingListViewLoadController(_loadController),
    );
  }

  @override
  void dispose() {
    _loadController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      child: PagewiseGridView.count(
        crossAxisCount: widget.crossAxisCount,
        mainAxisSpacing: 4.0,
        crossAxisSpacing: 4.0,
        childAspectRatio: widget.childAspectRatio,
        padding: widget.padding,
        itemBuilder: widget.itemBuilder,
        noItemsFoundBuilder:
            widget.noItemsFoundBuilder ?? (context) => const NoItemFound(),
        pageLoadController: _loadController,
      ),
      onRefresh: () async {
        _loadController.reset();
      },
    );
  }
}

/// A proxy list view to external list view widget.
/// We can customize this list view without updating the biz code.
///
/// [pageSize] determines how many items needed from server within one round trip.
/// [retrieveData] is the function which used to retieve the data. It returns [Future].
/// [itemBuilder] is the function which builds the list item for display.
/// [getLoadController] is a callback function to supply the [PagewiseLoadController] for any advance usage.
class PagingSliverGridView extends StatefulWidget {
  const PagingSliverGridView({
    super.key,
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    this.noItemsFoundBuilder,
    this.crossAxisCount = 2,
    this.childAspectRatio = 1.0,
    this.getLoadController,
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingViewItemBuilder itemBuilder;
  final NoItemsFoundBuilder? noItemsFoundBuilder;
  final int crossAxisCount;
  final double childAspectRatio;
  final GetLoadController? getLoadController;

  @override
  State<PagingSliverGridView> createState() => _PagingSliverGridViewState();
}

class _PagingSliverGridViewState extends State<PagingSliverGridView> {
  // Using [PagewiseLoadController] due to the needs of [RefreshIndicator].
  // We need to support the refresh operation when user pulls down on the top of the list.
  late final PagewiseLoadController _loadController = PagewiseLoadController(
    pageSize: widget.pageSize,
    pageFuture: widget.retrieveData,
  );

  @override
  void initState() {
    super.initState();

    widget.getLoadController?.call(
      PagingListViewLoadController(_loadController),
    );
  }

  @override
  void dispose() {
    _loadController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      child: PagewiseSliverGrid.count(
        crossAxisCount: widget.crossAxisCount,
        mainAxisSpacing: 4.0,
        crossAxisSpacing: 4.0,
        childAspectRatio: widget.childAspectRatio,
        itemBuilder: widget.itemBuilder,
        noItemsFoundBuilder:
            widget.noItemsFoundBuilder ?? (context) => const NoItemFound(),
        pageLoadController: _loadController,
      ),
      onRefresh: () async {
        _loadController.reset();
      },
    );
  }
}

class PagingStaggeredGridView extends StatefulWidget {
  const PagingStaggeredGridView({
    super.key,
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    this.noItemsFoundBuilder,
    this.padding = const EdgeInsets.all(6.0),
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingViewItemBuilder itemBuilder;
  final NoItemsFoundBuilder? noItemsFoundBuilder;
  final int crossAxisCount = 4;
  final EdgeInsetsGeometry padding;

  @override
  State<PagingStaggeredGridView> createState() =>
      _PagingStaggeredGridViewState();
}

class _PagingStaggeredGridViewState extends State<PagingStaggeredGridView> {
  // The default page index for initial loading.
  int _pageIndex = 0;

  // Indicates if the data is in loading progress. If true the loading indicator will be displayed.
  bool _isLoading = false;

  // Checks if more data exists and needs to load more data.
  bool _hasMoreData = true;

  // The data source list.
  List<dynamic> _list = [];

  @override
  void initState() {
    super.initState();

    // Loads the first page of the data.
    _retrieveData();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: RefreshIndicator(
            child: MasonryGridView.count(
              crossAxisCount: widget.crossAxisCount,
              primary: false,
              mainAxisSpacing: 4.0,
              crossAxisSpacing: 4.0,
              padding: widget.padding,
              itemCount: _list.length,
              itemBuilder: (context, index) {
                _tryToLoadMoreData(index);
                return widget.itemBuilder(context, _list[index], index);
              },
            ),
            onRefresh: () async {
              // Resets the [_pageIndex] to load the first page.
              _pageIndex = 0;

              // Loads the first page data.
              final res = await widget.retrieveData(_pageIndex);

              if (mounted) {
                // Triggers the build method.
                setState(() => _list = res);
              }
            },
          ),
        ),
        _isLoading ? const MyPageTopCenterLoadingIndicator() : Container(),
      ],
    );
  }

  Future<void> _retrieveData() async {
    final res = await widget.retrieveData(_pageIndex);

    if (mounted) {
      setState(() => _list = res);
    }
  }

  /// When the [itemBuilder] is called during [build] process we need to check if we can load more data.
  Future<void> _tryToLoadMoreData(int index) async {
    // How can we tell there is more data or not?
    // Checks on remainder of _list.length % widget.pageSize.
    //
    // Example:
    // If _list.length = 13 and pageSize = 10. _list.length % widget.pageSize == 3.
    // That means there is no 14th item and more. No more data.
    //
    // If _list.length = 20 and pageSize = 10. _list.length % widget.pageSize == 0.
    // There might be more data that we are not sure. Then we need to try to load more.
    //
    // There is one exception. If pageSize = 1 the remainder will be always greater than 0.
    // DO NOT USE 1 AS pageSize FOR TESTING. Otherwise there will be infinity loading.
    _hasMoreData =
        _list.length >= widget.pageSize && _list.length % widget.pageSize == 0;

    // Flutter only builds the widget which in particular range.
    // This makes us to do pagination a little bit tricky.
    // We only need to check if the last item called in build method.
    // It's almost means user is reaching the bottom of the list.
    // Now its a right time to get next page.
    // The build method will triggered twice. First for display the loading indicator. Second for update the list.
    // That's why we need to check if the data loading is done.
    if (index == _list.length - 1 && _hasMoreData && !_isLoading) {
      // Directly call [setState] can cause error. We need to call it asynchronously.
      //
      // This PagingStaggeredGridView widget cannot be marked as needing to build because
      // the framework is already in the process of building widgets.
      // A widget can be marked as needing to be built during the build phase
      // only if one of its ancestors is currently building. This exception is allowed
      // because the framework builds parent widgets before children,
      // which means a dirty descendant will always be built. Otherwise,
      // the framework might not visit this widget during this build phase.
      //
      // This is for display the loading indicator.
      Timer.run(() {
        if (mounted) {
          setState(() => _isLoading = true);
        }
      });

      // Prepares to retrieve next page of data.
      _pageIndex++;

      // Loads next page data.
      final res = await widget.retrieveData(_pageIndex);

      if (mounted) {
        // Triggers the build.
        setState(() {
          // If has data then add it to [_list].
          // If no more data then we need to cancal loading.
          if (res.isNotEmpty) {
            _list.addAll(res);
          }

          _isLoading = false;
        });
      }
    }
  }
}

/// Used by [PagingAnimatedList].
///
/// [pageSize] and [retrieveData] are for data loading.
/// [listKey] is for animated list item adding & removing.
class PagingAnimatedListViewLoadController extends ChangeNotifier {
  PagingAnimatedListViewLoadController({
    required this.pageSize,
    required this.retrieveData,
    required this.listKey,
  });

  static const defaultDuration = Duration(milliseconds: 300);

  final int pageSize;
  final RetrieveData retrieveData;
  final GlobalKey<AnimatedListState> listKey;
  final _list = [];

  // Sets the init page index.
  var pageIndex = 0;

  var _dataInitialized = false;

  /// Gets current data list length.
  int get listLength {
    return _list.length;
  }

  /// Checks if current data list is not empty.
  bool get hasData {
    return _list.isNotEmpty;
  }

  /// Checks if the data list is initialized at the first time.
  bool get dataInitialized {
    return _dataInitialized;
  }

  /// Gets item from data list with its [index].
  dynamic getItem(int index) {
    return index < listLength ? _list[index] : null;
  }

  /// Gets the item index.
  int getItemIndex(dynamic item) {
    return _list.indexOf(item);
  }

  /// Loads the first page data.
  Future<void> reset() async {
    // Resets page index.
    pageIndex = 0;

    // Clears the [_list] and adds all first page data to it.
    _list.clear();

    _dataInitialized = false;

    // Retrieves the first page data.
    final list = await retrieveData(0);

    // Adds data list to animated list.
    _addAllList(list);

    // Data initialization is done.
    _dataInitialized = true;

    // Notifies listener to take necessary action.
    notifyListeners();
  }

  /// Loads the data based on provided [pageIndex].
  Future<void> loadData(int pageIndex) async {
    final list = await retrieveData(pageIndex);
    _addAllList(list);
  }

  /// Loads next page data based on current page index.
  Future<void> loadNextPage() async {
    pageIndex++;
    loadData(pageIndex);
  }

  /// Remove the item at [index] and start an animation that will be passed
  /// to [builder] when the item is visible.
  void removeItem(
    int index,
    AnimatedRemovedItemBuilder builder, {
    Duration duration = defaultDuration,
  }) {
    // Removes item from data list.
    _list.removeAt(index);

    // Removes item from animated list.
    listKey.currentState!.removeItem(index, builder, duration: duration);
  }

  /// Insert an item at [index] and start an animation that will be passed
  /// to [AnimatedList.itemBuilder] when the item is visible.
  void insertItem(
    int index,
    dynamic item, {
    Duration duration = defaultDuration,
  }) {
    // Inserts item to data list.
    _list.insert(index, item);

    // Inserts item to animated list.
    listKey.currentState!.insertItem(index, duration: duration);
  }

  /// Adds [list] to the [_list].
  void _addAllList(List<dynamic> list) {
    // Iterates the input list and adds all items to [_list].
    for (final item in list) {
      insertItem(listLength, item);
    }
  }
}

/// The animated list supports pagination and pull-to-refresh features.
class PagingAnimatedList extends StatefulWidget {
  const PagingAnimatedList({
    required this.pageSize,
    required this.retrieveData,
    required this.itemBuilder,
    super.key,
    this.getLoadController,
  });

  final int pageSize;
  final RetrieveData retrieveData;
  final PagingAnimatedListItemBuilder itemBuilder;
  final GetLoadController? getLoadController;

  @override
  State<PagingAnimatedList> createState() => _PagingAnimatedListState();
}

class _PagingAnimatedListState extends State<PagingAnimatedList> {
  final _listKey = GlobalKey<AnimatedListState>();
  final _itemCountToListBottom = 3;

  // For loading, inserting and removing items.
  late final PagingAnimatedListViewLoadController _loadController =
      PagingAnimatedListViewLoadController(
        pageSize: widget.pageSize,
        retrieveData: widget.retrieveData,
        listKey: _listKey,
      );

  bool _dataLoaded = false;
  bool _hasData = false;

  ScrollController? _scrollController;

  @override
  void initState() {
    super.initState();

    _init();
  }

  @override
  void dispose() {
    // Clears the [PagingAnimatedListViewLoadController].
    _loadController.removeListener(_finishInit);
    _loadController.dispose();

    // Clears the [ScrollController].
    _scrollController?.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _loadController.reset,
      child: Column(
        children: [
          if (!_dataLoaded) const MyPageTopCenterLoadingIndicator(),
          if (_dataLoaded && !_hasData)
            const Padding(
              padding: EdgeInsets.only(top: 20.0),
              child: NoItemFound(),
            ),
          Expanded(
            child: AnimatedList(
              key: _listKey,
              initialItemCount: _loadController.listLength,
              //              controller: _scrollController,
              itemBuilder: (context, index, animation) {
                // Retrieves the next page data when reaching the list bottom.
                if (index ==
                    _loadController.listLength - _itemCountToListBottom) {
                  // Retrieves next page data.
                  _loadController.loadNextPage();
                }

                //                if (index > _loadController.listLength - 1) {
                //                  return Container();
                //                } else {
                //                  if (index == _loadController.listLength - 1) {
                //                    return Column(
                //                      children: [
                //                        widget.itemBuilder(context, index,
                //                            _loadController.getItem(index), animation),
                //                        MyCircularProgressIndicator(),
                //                      ],
                //                    );
                //                  } else {
                //                    return widget.itemBuilder(context, index,
                //                        _loadController.getItem(index), animation);
                //                  }
                //                }

                // [itemBuilder] is called with index which is out of the length range.
                // That's why need to check the index before return the widget.
                return index > _loadController.listLength - 1
                    ? Container()
                    : widget.itemBuilder(
                        context,
                        index,
                        _loadController.getItem(index),
                        animation,
                      );
              },
            ),
          ),
        ],
      ),
    );
  }

  /// Initialization for all necessary properties.
  void _init() {
    // For animated list scrolling after being reset.
    //    _scrollController = ScrollController();

    // Adds listener to check if data is initialized.
    _loadController.addListener(_finishInit);

    // Passes the [_loadController] to callback if needed.
    widget.getLoadController?.call(_loadController);

    // Loads the first page data.
    _loadController.reset();
  }

  /// Called after data initialized.
  void _finishInit() {
    if (mounted) {
      setState(() {
        _dataLoaded = _loadController.dataInitialized;
        _hasData = _loadController.hasData;
      });

      // Scrolls the list to the top.
      //      _scrollController.animateTo(0.0,
      //          duration: Duration(milliseconds: 200), curve: Curves.decelerate);
    }
  }
}
